Digitální neviditelný inkoust¶

Workshop na steganografii¶

Autoři: Martin Beneš, Verena Lachner

github.com/martinbenes1996/gyrec-stego-demo

Pojďme si zahrát s mincí!


Source: https://giphy.com/

$1000$ lidí si hodí $100\times$ mincí, a sečte počet orlů.

In [4]:
import numpy as np
toss = np.random.choice([0, 1], size=[1000, 100])  # vrhy
toss_sums = np.sum(toss, axis=1)  # secti orly
In [8]:
import seaborn as sns
sns.histplot(toss_sums, discrete=True);  # histogram

Histogramy umožňují analyzovat rozdělení dat graficky.

Hra s nepoctivou mincí


Source: https://giphy.com/

Na naší nepoctivé minci padá orel s pravděpodobností $52\%$.

In [9]:
toss2 = np.random.choice([0, 1], size=[1000, 100], p=[.48, .52])
toss2_sums = np.sum(toss2, axis=1)
In [10]:
sns.histplot(toss_sums, discrete=True, label='0.5');  # poctiva
sns.histplot(toss2_sums, discrete=True, label='0.52');  # nepoctiva
plt.legend();

Liší se oranžový histogram od modrého?

Můžeme si to otestovat statisticky.

$\chi^2$ test porovnává dva histogramy, $h$ a $g$.

$$X^2=\sum_{i=1}^{N}\frac{(h_i - g_i)^2}{h_i}$$
In [34]:
# histogramy
h = np.histogram(toss_sums, bins=range(100+1))[0]
g = np.histogram(toss2_sums, bins=range(100+1))[0]

# testovaci statistika
test_stat = 0
for i in range(100):
    if h[i] > 0:
        test_stat += (h[i] - g[i])**2 / h[i]
In [12]:
test_stat  # testovaci statistika
Out[12]:
197.7241088701197

Převedeme si testovací statistiku na p-hodnotu (pomocí $\chi^2$ rozdělení).

In [13]:
scipy.stats.chi2.sf(test_stat, 100-1)  # vyznamne kdyz <0.05
Out[13]:
1.4826819136900469e-08

Histogramy se liší. Něco je špatně s naší mincí!

Dost bylo mincí, zpět k obrázkům!


Source: https://giphy.com/
In [14]:
x = np.array(Image.open('hora.png'))  # nahraj obrazek
plt.imshow(x,cmap = "gray");
In [15]:
sns.histplot(x.flatten(), discrete=True);  # histogram obalky
In [16]:
y = lsbr(x, 1.)  # ukryj steganografii
sns.histplot(y.flatten(), discrete=True);  # stego histogram

Podívejme se zblízka. Co se nám to děje s histogram?

In [17]:
fig, ax = plt.subplots(1, 3, sharey=True)
for i, alpha in enumerate([.0, .5, 1.]):
    y = lsbr(x, alpha).flatten()
    sns.histplot(y, binrange=(125, 175), discrete=True, ax=ax[i]);
    ax[i].set_title(f'{alpha:.1f}');

LSB nahrazování způsobuje, že se vyrovnávají sousední sloupce (sudý a lichý).

$$\bar{h}_i=\frac{h_{i}+h_{i+1}}{2}$$
In [18]:
# histogram
h, edges = np.histogram(x.flatten(), bins=range(256+1))
# prumer sudy-lichy
hbar = np.repeat((h[:-1:2] + h[1::2]) / 2, 2)
In [19]:
fig, ax = plt.subplots(1, 2, sharey=True)
ax[0].bar(range(256), h);
ax[1].bar(range(256), hbar);

Jak můžeme využít $\chi^2$ test pro zjištění přítomnosti steganografie?

Když se histogram podobá tomu se zprůměrovanými páry, steganografie je přítomna.

$$S=\sum_{i=0}^{255}\frac{(h_i-\bar{h}_i)^2}{\bar{h}_i}$$
In [20]:
# zamezeni deleni nulou
h = h[hbar > 0]
hbar = hbar[hbar > 0]
# chi2 test
test_stat = np.sum((h - hbar)**2 / hbar)
pvalue = scipy.stats.chi2.sf(test_stat, 2**8-1)
In [21]:
pvalue  # stego kdyz >0.05
Out[21]:
0.0

P-hodnota je menší než $5\%$, histogramy se liší. Obrázek je obálka.

Provedeme $\chi^2$-test na stego obrázku.

In [22]:
def chi2_attack(x):
    # histograms
    h = np.histogram(x.flatten(), bins=range(256+1))[0]
    hbar = np.repeat((h[:-1:2] + h[1::2])/2, 2)
    h, hbar = h[hbar > 0], hbar[hbar > 0]
    # chi2 test
    S = np.sum((h[:-1:2] - hbar[::2])**2 / hbar[::2])
    return scipy.stats.chi2.sf(S, h.size-1)
In [23]:
y = lsbr(x, 1.)  # vytvor stego
pvalue = chi2_attack(y)  # chi2 utok
In [24]:
pvalue  # stego kdyz >0.05
Out[24]:
1.0

P-hodnota je větší než $5\%$, histogramy se liší. Obrázek je stego.

Shrnutí¶

  • Steganografie narušuje statistické vlastnosti obrázků.
  • $\chi^2$ test umí detekovat přítomnost steganografie LSB nahrazováním.

Hands-on: vyřešení případu¶

  • Mezi podezřelými obrázky, najděte ten/ty s ukrytou zprávou
  • Zkuste zprávu/y extrahovat
    • Tajný klíč je odpověď na otázku života, vesmíru a vůbec.
In [27]:
# nacti podezrele obrazky
dum = np.array(Image.open('dum.png'))
socha = np.array(Image.open('socha.png'))
stena = np.array(Image.open('stena.png'))

Zkus $\chi^2$ test na každém obrázku.

In [28]:
chi2_attack(dum)
Out[28]:
0.0
In [29]:
chi2_attack(socha)
Out[29]:
0.0
In [30]:
chi2_attack(stena)
Out[30]:
1.0
In [31]:
key = 42  # odpoved na otazku zivota
message = extract_lsbr(stena, key=key)
In [32]:
print(message[:300], '...')
Božena Němcová: Babička

Úvod
Dávno, dávno již tomu, co jsem posledně se dívala do té milé
mírné tváře, co jsem zulíbala to bledé líce, plné vrásků, nahlížela do
modrého oka, v němž se jevilo tolik dobroty a lásky, dávno tomu, co
mne posledně žehnaly staré její ruce! - Není více dobré stařenky!
Dáv ...
In [33]:
print(extract_lsbr(dum))  # ukryty Easter egg
Cau Vereno!
Je mi lito, ze jsi ztratila klic od kola.
Co se dnes potkat u stojanu na kola, abychom odrizli tvuj zamek.

Martin.